<?php
/**
* This file belongs to a webappkit package (kit)
* @link http://webappkit.net
* @package datapath
* @author J.Ducastel <nospam0@ducastel.name>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/

/**
* accessing multidimensional data (object vars or array) through path syntax.
* <p>this is a static method, works on data structures by reference</p>
* <p>default syntax is "dot" i.e. some.path.to.data = $data['some']['path']['to']['data']</p>
* @static
* @author J.Ducastel <nospam0@ducastel.name>
*/
class DataPath {
/*--------------------------------------------------------------------
PUBLIC STATIC METHODS
--------------------------------------------------------------------*/
    /**
    * set or get path segments separator character.
    * @static
    * @param string $new optional, single character, new separator
    * @access public
    * @return string
    */
    public static function separator($new=null) {
        static $separator='.';
        if (is_string($new) and strlen($new)==1)
            $separator=$new;
        return $separator;
    }

    /**
     * escape separator in a key
     * @static
     * @param string $key to escape
     * @return string escaped key
     */
    public static function escapeKey($key) {
        return str_replace(self::separator(),chr(92).self::separator(),$key);
    }

        /**
     * escape separator in a key
     * @static
     * @param string $key to escape
     * @return string escaped key
     */
    public static function unEscapeKey($key) {
        return str_replace(chr(92).self::separator(),self::separator(),$key);
    }

    /**
     * tells if a key is set
     * @static
     * @param mixed &$data array or object
     * @param string $path
     * @access public
     * @return bool
     */
    public static function has(&$data,$path) {
        // parsing path
        $childkey=datapath::cutpath($path);
        // checking next node
        if (!Datapath::hasChild($data,$childkey))
            return false;
        // getting next node
        $child=&datapath::getchild($data,$childkey);
        if (strlen($path)==0)
            // branch exist, end of path
            return true;
        // branch exist,path continue
        return datapath::has($child,$path);
    }

    /**
    * get targeted path from data
    * @static
    * @param mixed &$data array or object
    * @param string $path
    * @access public
    * @return mixed targeted path, by reference
    */
    public static function & get(&$data, $path=null) {
        if (is_null($path)
        or (is_string($path) and strlen($path)==0))
            // path is blank, returning root
            return $data;
        // parsing path
        $childkey=datapath::cutpath($path);
        // reaching target
        // getting branch
        $child=&datapath::getchild($data,$childkey);
        if (!$child)
            // branch doesn't exist
            return $child;
        if (strlen($path)==0)
            // branch exist, end of path
            return $child;
        // branch exist,path continue
        $target=&datapath::get($child,$path);
        return $target;
    }/**/

    /**
    * sets targeted path value.
    * overwrite existing members if needed
    * creates members if needed
    * @static
    * @param array|object &$data
    * @param mixed $value
    * @param string $path
    * @access public
    * @return bool
    */
    public static function set(&$data,$value,$path=null) {
        //echo '<p>setting '.$path.' as '.print_r($value).'</p>';
        // parsing path
        if (!$branch=datapath::cutpath($path)) {
            // setting root
            $data=$value;
            return true;
        }
        // is target reached ?
        if (strlen($path)==0) {
            return datapath::setchild($data,$branch,$value);
        }
        // continue along the path
        $child=&datapath::getchild($data,$branch);
        //if (!is_array($child) or !is_object($child))
        if (!$child)
            $child=&datapath::makechild($data,$branch);
        return datapath::set($child,$value,$path);
    }/**/

    /**
    * sets a key by reference
    * @static
    * @param array &$data source data
    * @param mixed &$ref reference
    * @param string $path
    * @access public
    * @return bool
    */
    public static function setRef(&$data,&$ref,$path=null) {
        //echo "<p>setting ref on $path</p>";
        // is root ?
        if (is_null($path)
        or is_string($path) and strlen($path)==0) {
            $data=&$ref;
            return true;
        }
        // parsing path : substracting next branch from path
        if (!$branch=datapath::cutpath($path)) {
            return false;
        }
        // is target reached ?
        if (strlen($path)==0) {
            //echo "<p>target reached : $branch</p>";
            return datapath::setchild($data,$branch,$ref);
        }
        // continue along the path
        //echo "<p>continuing on $branch</p>";
        // getting or creating the child at branch
        if (!$child=&datapath::getchild($data,$branch))
            $child=&datapath::makechild($data,$branch);
        // print_r($child);
        return datapath::setref($child,$ref,$path);
    }/**/

    /**
    * return child keys for a path if available
    * @static
    * @param array &$data
    * @param string $path
    * @access public
    * @return array|false
    */
    public static function getChildKeys(&$data,$path=null) {
        if (!$target=&datapath::get($data,$path))
            return false;
        switch (true) {
            case is_array($target):
                return array_keys($target);
            case is_object($target):
                return array_keys(get_object_vars($target));
            default:
                return false;
        }
    }/**/

    /**
    * loops through path
    * @param array|object &$data
    * @param string $path
    * @access public
    * @return mixed
    * @todo
    * /
    function loop(&$data, $path) {
        static $looping=false;
        static $keys=array();
        static $ifni
    }/**/

    /**
    * Merges recursiveley two data structures
    * @static
    * @param array $data1
    * @param array $data2
    * @access public
    * @return array
    */
    public static function & merge($data1, $data2) {
        // will be the merged data
        $merged=$data1;
        // getting keys to compare
        $keys1=datapath::getchildkeys($data1);
        //print_r($keys1);
        // looping through first keys
        foreach ($keys1 as $key) {
            // is the key present in both data structs ?
            if (!datapath::getchild($data2,$key))
                // only in data1, passing
                continue;
            // getting values to compare
            $value1=datapath::get($data1,$key);
            $value2=datapath::getchild($data2,$key);
            // are both values arrays ?
            if (is_array($value1) and is_array($value2)) {
                // recursion
                $newvalue=datapath::merge($value1,$value2);
            } else {
                $newvalue=$value2;
            }
            datapath::set($merged,$newvalue,$key);
        }
        /*foreach ($merged as $key => $value) {
            if (!isset($array_2[$key]))
                continue;
            if (is_array($value) and is_array($array_2[$key])) {
                // appel recursif
                $merged[$key]=$this->array_merge_recursive_replace($value,$array_2[$key]);
            } else {
                $merged[$key]=$array_2[$key];
            }
        }*/
        // adding new keys : looping through data2
        $keys2=datapath::getchildkeys($data2); //print_r($keys2);
        foreach ($keys2 as $key) {
            if (!datapath::getchild($merged,$key))
                datapath::set($merged,datapath::get($data2,$key),$key);
        }
        /*foreach ($array_2 as $key => $value)
            if (!isset($merge[$key])) $merge[$key]=$value;*/
        return $merged;
    }/**/

/*--------------------------------------------------------------------
for self use
--------------------------------------------------------------------*/
    /**
    * remove next path segment from passed path, and returns it
    * @static
    * @param string &$path
    * @access protected
    * @return string next segment
    */
    protected static function cutPath(&$path) {
        if (!is_string($path))
            return false;
        $oldpath=$path;
        ereg('^(([^.]|\\\.)+)',$path,$found);
        // echo '<br/><pre>'; print_r($found);
        $path=substr($path,strlen($found[1])+1);
        $key=self::unEscapeKey($found[1]); // echo "<p>$path => $key</p>";
        return $key;
    }/**/

    /**
    * check if child exists
    * @static
    * @param array|object &$data source data
    * @param string $key
    * @access protected
    * @return bool
    */
    protected static function hasChild(&$data,$key) {
        // unescaping separator
        //$key=self::unEscapeKey($key);
        switch (true) {
            case is_array($data):
                return array_key_exists($key,$data);
            case is_object($data):
                return array_key_exists($key,get_object_vars($data));
            default:
                return false;
        }
    }/**/

    /**
    * return data member if exists
    * @static
    * @param array|object &$data source data
    * @param string $key
    * @access protected
    * @return mixed
    */
    protected static function & getChild(&$data,$key) {
        // unescaping separator
        //$key=self::unEscapeKey($key);
        switch (true) {
            /*case !is_string($key):
                trigger_error("key $key is not valid");
                break;*/
            // array, getting member
            case is_array($data) and isset($data[$key]):
                $child=&$data[$key];
                break;
            // object, getting member directly
            case is_object($data) and isset($data->$key):
                // echo "$key from object";
                $child=&$data->$key;
                break;
            // object, getting method result
            case is_object($data) and method_exists($data,$key):
                $child=$data->$key();
                break;
            default:
                $child=null;
        }
        return $child;
    }/**/

    /**
    * sets data member
    * @static
    * @param array|object &$data source data
    * @param string $key
    * @param mixed $value
    * @access protected
    * @return bool
    */
    protected static function setChild(&$data,$key, &$value) {
        switch (true) {
            // array, setting member
            case is_array($data):
                $data[$key]=&$value;
                return true;
                break;
            // object, accessing member
            case is_object($data) and isset($data->$key):
                $data->$key=&$value;
                return true;
            // object, member not set
            case is_object($data): // print_r($data);
                // can't assign by reference
                // if object overloads accessors __get and _set
                $data->$key=$value;
                return true;
            default:
                // trigger_error("can't set child $key as data is scalar");
                // making it array
                $data=array();
                $data[$key]=&$value;
                return false;
        }
        // return $child;
    }/**/

    /**
    * creates data member and return its reference
    * @static
    * @param array|object &$data source data
    * @param string $key
    * @param mixed $value
    * @access protected
    * @return bool
    */
    protected static function & makeChild(&$data,$key) {
        //echo "before :";
        //var_dump($data);
        switch (true) {
            // array, setting member
            case is_array($data):
                // target is an array
                $data[$key]=true;
                $child=&$data[$key];
                break;
            // object, setting member
            case is_object($data) and isset($data->$key):
                $data->$key=true;
                $child=&$data->$key;
                break;
            // object, method
            /*case is_object($data) and method_exists($data,$key):
                echo "<p>method $key from object</p>";
                $child=$data->$key($value);
                //return true;
                break;*/
            default:
                // making it array
                //echo "data was scalar : ".wakdebug::vardump($data);
                $data=array();
                $data[$key]=true;
                $child=&$data[$key];
                break;
                //$child=array();
                //$data=array($key=>true);
                //$child=&$data[$key];
                //$newdata=array($key=>&$child);
                //$data=&$newdata;
                //$data[$key]=&$child;
        }
        //echo "after :";
        //var_dump($data);
        return $child;
    }/**/

    /**
    * checks path validity
    * @static
    * @param string $path
    * @access protected
    * @return bool
    */
    protected static function checkPath($path) {
        return is_string($path);
    }/**/
}
